//////////////////////////////////////////////////////////////////////////////
//
//  Created by Philip Mulcahy on 07/06/2012.
//  Copyright (c) 2012 Philip Mulcahy. All rights reserved.
//
//  This file is part of the note-recog library.
//
//  note-recog is free software: you can redistribute it and/or modify
//  it under the terms of version 3 of the GNU Lesser General Public License 
//  as published by the Free Software Foundation.
//
//  note-recog is distributed is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU General Public License for more details.
//
//  You should have received a copy of the GNU General Public License
//  along with note-recog. If not, see <http://www.gnu.org/licenses/>.
//
//////////////////////////////////////////////////////////////////////////////

#include <iostream>
#include <limits>
#include <functional>
#include "Optimiser.h"
#include <sstream>

LOG_DEFINE(Optimiser);

static std::vector<std::string> extractNames(Optimiser::input_ranges ranges)
{
    std::vector<std::string> names;
    names.reserve(ranges.size());
    for( const Optimiser::input_ranges::value_type& it : ranges) {
        const std::string name = it.first;
        names.push_back(name);
    }
    return names;
}

Optimiser::Optimiser(target_func func,
          const input_ranges& ranges)
: _target(func),
  _argNames(extractNames(ranges)),
  _ranges(ranges),
  _optimalOutput(-std::numeric_limits<float>::max()),
  _perimeterOptimumOutput(-std::numeric_limits<float>::max())
{
    const single_input in;
    Optimiser::coverSpace(in);
    if( ! valid() ) {
        std::ostringstream oss;
        oss << "optimimum value of " << _perimeterOptimumOutput << " touches perimeter at: "
            << _perimeterOptimalInputs;
        LOG_THROW( InvalidOptimisationException(oss.str()) );
    }
}

Optimiser::~Optimiser()
{
}

void Optimiser::coverSpace(const single_input& input)
{
    single_input args = input;
    const size_t achieved = args.size();
    const size_t required = _ranges.size();
    const size_t shortfall = required - achieved;
    const bool met = shortfall <= 0;
    
    if( met ) {
        const float score = _target(args);
        const bool perimeter = onPerimeter(args);
        const bool qualifies = score >= _optimalOutput;
        const bool qualifiesPerim = perimeter && ( score >= _perimeterOptimumOutput );
        const bool newOpt = score > _optimalOutput;
        const bool newPerimOpt = perimeter && ( score > _perimeterOptimumOutput );
        if( perimeter ) {
            args.setComment( "perimeter" );
        }
        if( newOpt ) {
            _optimalInputs.clear();
            _optimalOutput = score;
        }
        if( newPerimOpt ) {
            _perimeterOptimalInputs.clear();
            _perimeterOptimumOutput = score;
        }
        if( qualifies ) {
            _optimalInputs.insert(args);
        }
        if( qualifiesPerim ) {
            _perimeterOptimalInputs.insert(args);
        }
        return;
    }
    
    const std::string& nextName = _argNames[achieved];
    const arg_range& range = _ranges.find(nextName)->second;
    for( const float& it : range ) {
        const float argVal = it;
        args[nextName] = argVal;
        coverSpace(args);
    }
}

Optimiser::arg_range::arg_range()
: _allowLowerBoundPerimeterTouch(false)
{}

Optimiser::arg_range::arg_range( const char * name )
: _allowLowerBoundPerimeterTouch(false),
  _name(name)
{}

const std::string& Optimiser::arg_range::name() const
{
    return _name;
}

void Optimiser::arg_range::setUp(
                                 float midPoint, 
                                 float halfRangeRatio, 
                                 float intervalRatio ) {
    const float first = midPoint / halfRangeRatio;
    const float limit = midPoint * halfRangeRatio;
    for( float value = first; value <= limit; value *= intervalRatio ) {
        insert( value );
    }
}

void Optimiser::input_ranges::add(const Optimiser::arg_range& range)
{
    const std::string name = range.name();
    (*this)[name] = range;
}

Optimiser::single_input Optimiser::input_set::centroid() const
{
    typedef std::pair<float,size_t> pair;
    typedef std::map<std::string,pair> intermediate;
    intermediate sumsandcounts;

    for( input_set::value_type i : _data ) {
        for( single_input::value_type j : i ) {
            const std::string& name = j.first;
            const float value = j.second;
            if( sumsandcounts.find(name) == sumsandcounts.end() ) {
                sumsandcounts[name] = pair(0.f,0);
            }
            pair& p = sumsandcounts[name];
            p.first += value;
            ++(p.second);
        }
    }
    
    single_input result;
    for( intermediate::value_type &i : sumsandcounts ) {
        const std::string name = i.first;
        const pair& p = i.second;
        const float sum = p.first;
        const size_t count = p.second;
        const float mean = sum / count;
        result[name] = mean;
    }
    
    return result;
}

float Optimiser::getPeak(input_set * inputs) const
{
    if( inputs ) {
        *inputs = _optimalInputs;
    }
    return _optimalOutput;
}

bool Optimiser::onPerimeter( const single_input& input ) const
{
    for( const input_ranges::value_type &rangePair : _ranges ) {
        const std::string& rangeName = rangePair.first;
        const arg_range& argRange = rangePair.second;
        const float low = *(argRange.begin());
        const float high = *(argRange.rbegin());
        const bool ignoreLowerBoundTouch = argRange.ignoreLowerBoundPerimeterTouch();
        const float val = input.find(rangeName)->second;
        if( !ignoreLowerBoundTouch ) {
            if( val == low ) {
                return true;
            }
        }
        if( val == high ) {
            return true;
        }
    }
    return false;
}

bool Optimiser::valid() const
{
    return _perimeterOptimumOutput < _optimalOutput;
}

void Optimiser::single_input::setComment(const char *comment)
{
    if( _comment.empty() )
    {
        _comment = std::string(comment);
    }
}

Optimiser::InvalidOptimisationException::InvalidOptimisationException( 
    const std::string& desc )
: _description( std::string("Optimiser::InvalidOptimisationException{") + desc + "}" )
{}

const char * Optimiser::InvalidOptimisationException::what() const throw()
{
    return _description.c_str();
}

std::ostream& operator<<(std::ostream& os, const Optimiser::input_set& input)
{
    os << "Optimiser::input_set{";
    for( Optimiser::input_set::const_iterator it = input.begin();
        it != input.end();
        ++it ) {
        if( it != input.begin() ) {
            os << std::endl;
        }
        const Optimiser::single_input& single = *it;
        os << single;
    }
    os << "}";
    return os;
}

std::ostream& operator<<(std::ostream& os, const Optimiser::single_input& input)
{
    os << "Optimiser::single_input{";
    for( Optimiser::single_input::const_iterator it = input.begin();
        it != input.end();
        ++it ) {
        if( it != input.begin() ) {
            os << ",";
        }
        os << it->first << "=" << it->second;
    }
    if( strlen(input.comment()) ) {
        os << ":" << input.comment();
    }
    os << "}";
    return os;
}

std::ostream& operator<<(std::ostream& os, const Optimiser::arg_range& range)
{
    os << "Optimiser::arg_range{" << range.name() << ":";
    for( Optimiser::arg_range::const_iterator it = range.begin();
        it != range.end();
        ++it ) {
        if( it != range.begin() ) {
            os << ",";
        };
        os << *it;
    }
    os << "}";
    return os;
}

std::ostream& operator<<(std::ostream& os, const Optimiser::input_ranges& ranges)
{
    os << "Optimiser::input_ranges{";
    for( Optimiser::input_ranges::const_iterator it = ranges.begin();
        it != ranges.end();
        ++it ) {
        if( it != ranges.begin() ) {
            os << ",";
        };
        os << it->first << ":" << it->second;
    }
    os << "}";
    return os;
}